/*
 * Copyright 2016 OrientDB LTD (info(at)orientdb.com)
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 *   For more information: http://www.orientdb.com
 */
package com.orientechnologies.agent;

import com.orientechnologies.agent.functions.OAgentProfilerService;
import com.orientechnologies.agent.ha.OEnterpriseDistributedStrategy;
import com.orientechnologies.agent.http.command.OServerCommandDistributedManager;
import com.orientechnologies.agent.profiler.OEnterpriseProfiler;
import com.orientechnologies.agent.services.OEnterpriseService;
import com.orientechnologies.agent.services.backup.OBackupService;
import com.orientechnologies.agent.services.distributed.ODistributedService;
import com.orientechnologies.agent.services.metrics.OrientDBMetricsService;
import com.orientechnologies.agent.services.security.OSecurityService;
import com.orientechnologies.agent.services.studio.StudioService;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.log.OLogger;
import com.orientechnologies.common.profiler.OAbstractProfiler.OProfilerHookValue;
import com.orientechnologies.common.profiler.OProfiler;
import com.orientechnologies.enterprise.server.OEnterpriseServer;
import com.orientechnologies.enterprise.server.OEnterpriseServerImpl;
import com.orientechnologies.orient.core.OConstants;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.db.ODatabaseLifecycleListener;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.enterprise.OEnterpriseEndpoint;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.distributed.ONodeConfig;
import com.orientechnologies.orient.server.OClientConnection;
import com.orientechnologies.orient.server.OServer;
import com.orientechnologies.orient.server.OServerLifecycleListener;
import com.orientechnologies.orient.server.config.OServerParameterConfiguration;
import com.orientechnologies.orient.server.distributed.ODistributedConfiguration;
import com.orientechnologies.orient.server.distributed.ODistributedServerManager;
import com.orientechnologies.orient.server.distributed.OModifiableDistributedConfiguration;
import com.orientechnologies.orient.server.distributed.impl.ODatabaseDocumentDistributed;
import com.orientechnologies.orient.server.distributed.impl.ODistributedPlugin;
import com.orientechnologies.orient.server.network.OServerNetworkListener;
import com.orientechnologies.orient.server.network.protocol.http.ONetworkProtocolHttpAbstract;
import com.orientechnologies.orient.server.plugin.OPluginLifecycleListener;
import com.orientechnologies.orient.server.plugin.OServerPlugin;
import com.orientechnologies.orient.server.plugin.OServerPluginAbstract;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Properties;

public class OEnterpriseAgent extends OServerPluginAbstract
    implements ODatabaseLifecycleListener,
        OPluginLifecycleListener,
        OServerLifecycleListener,
        OEnterpriseEndpoint {
  private static final OLogger logger = OLogManager.instance().logger(OEnterpriseAgent.class);

  private static final String PLUGIN_NAME = "enterprise-agent";
  private static final String PATH_TO_EE_AGENT_PROPS = "/com/orientechnologies/agent.properties";
  private static final String EE_VERSION = "version";
  private static final boolean PLUGIN_ENABLED_DEFAULT = false;

  private String enterpriseVersion = "";
  public OServer server;
  private Properties properties = new Properties();

  private List<OEnterpriseService> services = new ArrayList<>();

  private OEnterpriseServer enterpriseServer;

  public OEnterpriseAgent() {}

  @Override
  public void config(final OServer oServer, final OServerParameterConfiguration[] iParams) {
    enabled = PLUGIN_ENABLED_DEFAULT;
    server = oServer;

    enterpriseServer = new OEnterpriseServerImpl(server, this);

    if (oServer.getPluginManager() != null) {
      oServer.getPluginManager().registerLifecycleListener(this);
    }
    registerAndInitServices();
  }

  private void registerAndInitServices() {
    this.services.add(new StudioService());
    this.services.add(new OrientDBMetricsService());
    this.services.add(new OAgentProfilerService());
    this.services.add(new OBackupService());
    this.services.add(new OSecurityService());
    this.services.add(new ODistributedService());

    this.services.forEach((s) -> s.init(this.enterpriseServer));
  }

  @Override
  public String getName() {
    return PLUGIN_NAME;
  }

  @Override
  public void startup() {
    try {
      loadAgentProperties();
      if (checkLicense() && checkVersion()) {
        server.registerLifecycleListener(this);
        enabled = true;
        Orient.instance().addDbLifecycleListener(this);
      }
    } catch (final Exception e) {
      logger.warn("Error loading agent.properties file. EE will be disabled: %s", e.getMessage());
    }
  }

  @Override
  public void shutdown() {
    if (enabled) {
      uninstallCommands();
      if (server.getPluginManager() != null) {
        server.getPluginManager().unregisterLifecycleListener(this);
      }
      Orient.instance().removeDbLifecycleListener(this);
    }
  }

  // TODO SEND CPU METRICS ON configuration request;
  @Override
  public void onLocalNodeConfigurationRequest(ONodeConfig iConfiguration) {
    final OProfiler profiler = Orient.instance().getProfiler();
    if (profiler instanceof OEnterpriseProfiler) {
      iConfiguration.setCpu(((OEnterpriseProfiler) profiler).cpuUsage());
    }
  }

  @Deprecated
  public void installDistributedCommands() {
    final OServerNetworkListener listener =
        server.getListenerByProtocol(ONetworkProtocolHttpAbstract.class);
    if (listener == null) throw new OConfigurationException("HTTP listener not found");
  }

  private void uninstallCommands() {
    final OServerNetworkListener listener =
        server.getListenerByProtocol(ONetworkProtocolHttpAbstract.class);
    if (listener == null) {
      throw new OConfigurationException("HTTP listener not found");
    }
    listener.unregisterStatelessCommand(OServerCommandDistributedManager.class);
  }

  private boolean checkLicense() {
    logger.info("");
    logger.info("*****************************************************************************");
    logger.info("*                     ORIENTDB  -  ENTERPRISE EDITION                       *");
    logger.info("*****************************************************************************");
    logger.info("");
    Orient.instance()
        .getProfiler()
        .registerHookValue(
            Orient.instance().getProfiler().getSystemMetric("config.agentVersion"),
            "Enterprise License",
            OProfiler.METRIC_TYPE.TEXT,
            new OProfilerHookValue() {
              @Override
              public Object getValue() {
                return enterpriseVersion;
              }
            });
    return true;
  }

  private void loadAgentProperties() throws IOException {
    final InputStream inputStream =
        OEnterpriseAgent.class.getResourceAsStream(PATH_TO_EE_AGENT_PROPS);
    try {
      properties.load(inputStream);
      enterpriseVersion = properties.getProperty(EE_VERSION);
      if (enterpriseVersion == null || enterpriseVersion.isEmpty()) {
        throw new IllegalArgumentException(
            "Cannot read the agent version from the agent config file.");
      }
    } finally {
      try {
        inputStream.close();
      } catch (IOException e) {
        logger.warn("Failed to close input stream %s", e, inputStream);
      }
    }
  }

  private boolean checkVersion() {
    if (OConstants.getRawVersion().equalsIgnoreCase(enterpriseVersion)) {
      return true;
    }

    String ceLower = OConstants.getRawVersion().toLowerCase(Locale.ENGLISH);
    String eeLower = enterpriseVersion.toLowerCase(Locale.ENGLISH);

    if (ceLower.contains("-snapshot") && eeLower.contains("snapshot")) {
      ceLower.replace("-snapshot", "");
      eeLower.replace("-snapshot", "");
    }

    if (eeLower.contains("-sap")) {
      eeLower = eeLower.substring(0, eeLower.indexOf("-sap"));
    }
    if (eeLower.equals(ceLower)) {
      return true;
    }

    logger.warn(
        "The current agent version %s is not compatible with OrientDB %s. Please use the same"
            + " version.",
        enterpriseVersion, OConstants.getVersion());
    return false;
  }

  private void installComponents() {
    if (server.getDistributedManager() != null) {
      server.getDistributedManager().setDistributedStrategy(new OEnterpriseDistributedStrategy());
    }
  }

  // OPluginLifecycleListener
  public void onBeforeConfig(
      final OServerPlugin plugin, final OServerParameterConfiguration[] cfg) {}

  public void onAfterConfig(
      final OServerPlugin plugin, final OServerParameterConfiguration[] cfg) {}

  public void onBeforeStartup(final OServerPlugin plugin) {
    if (plugin instanceof ODistributedServerManager) {
      installComponents();
    }
  }

  public void onAfterStartup(final OServerPlugin plugin) {}

  public void onBeforeShutdown(final OServerPlugin plugin) {}

  public void onAfterShutdown(final OServerPlugin plugin) {}

  @Override
  public void onBeforeClientRequest(final OClientConnection iConnection, final byte iRequestType) {}

  public boolean isDistributed() {
    return server.getDistributedManager() != null;
  }

  public ODistributedServerManager getDistributedManager() {

    return server.getDistributedManager();
  }

  public String getNodeName() {
    return isDistributed() ? server.getDistributedManager().getLocalNodeName() : "orientdb";
  }

  @Override
  public void onBeforeActivate() {}

  @Override
  public void onAfterActivate() {
    services.forEach((s) -> s.start());
  }

  @Override
  public void onBeforeDeactivate() {
    services.forEach(
        (s) -> {
          s.stop();
        });
  }

  @Override
  public void onAfterDeactivate() {}

  @Override
  public void onAfterClientRequest(OClientConnection iConnection, byte iRequestType) {
    super.onAfterClientRequest(iConnection, iRequestType);
  }

  public <T extends OEnterpriseService> Optional<T> getServiceByClass(Class<T> klass) {
    return (Optional<T>) this.services.stream().filter(c -> c.getClass().equals(klass)).findFirst();
  }

  @Override
  public void haSetDbStatus(
      final ODatabaseDocument database, final String nodeName, final String status) {
    database.checkSecurity(ORule.ResourceGeneric.SERVER, "status", ORole.PERMISSION_UPDATE);
    if (!(database instanceof ODatabaseDocumentDistributed)) {
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");
    }
    final ODistributedPlugin dManager =
        (ODistributedPlugin) ((ODatabaseDocumentDistributed) database).getDistributedManager();
    if (dManager == null || !dManager.isEnabled()) {
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");
    }
    final String databaseName = database.getName();
    dManager.getDatabaseConfiguration(databaseName);
    dManager.setDatabaseStatus(
        nodeName, databaseName, ODistributedServerManager.DB_STATUS.valueOf(status));
  }

  @Override
  public void haSetRole(ODatabaseDocument database, String serverName, String role) {
    database.checkSecurity(ORule.ResourceGeneric.SERVER, "status", ORole.PERMISSION_UPDATE);

    if (!(database instanceof ODatabaseDocumentDistributed)) {
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");
    }

    final ODistributedPlugin dManager =
        (ODistributedPlugin) ((ODatabaseDocumentDistributed) database).getDistributedManager();
    if (dManager == null || !dManager.isEnabled()) {
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");
    }
    final String databaseName = database.getName();
    final ODistributedConfiguration cfg = dManager.getDatabaseConfiguration(databaseName);
    final OModifiableDistributedConfiguration newCfg = cfg.modify();
    newCfg.setServerRole(serverName, ODistributedConfiguration.ROLES.valueOf(role));
    dManager.updateCachedDatabaseConfiguration(databaseName, newCfg);
  }

  @Override
  public void haSetOwner(ODatabaseDocument database, String clusterName, String owner) {
    database.checkSecurity(ORule.ResourceGeneric.SERVER, "status", ORole.PERMISSION_UPDATE);
    if (!(database instanceof ODatabaseDocumentDistributed)) {
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");
    }
    final ODistributedPlugin dManager =
        (ODistributedPlugin) ((ODatabaseDocumentDistributed) database).getDistributedManager();
    if (dManager == null || !dManager.isEnabled()) {
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");
    }
    final String databaseName = database.getName();
    final ODistributedConfiguration cfg = dManager.getDatabaseConfiguration(databaseName);
    final OModifiableDistributedConfiguration newCfg = cfg.modify();
    newCfg.setServerOwner(clusterName, owner);
    dManager.updateCachedDatabaseConfiguration(databaseName, newCfg);
  }

  public OEnterpriseServer getEnterpriseServer() {
    return enterpriseServer;
  }
}
